package edu.fudan.p2p.chord;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class Node {

    Map<Integer, Node> networks;
    int[] fingerTable;
    int ID;
    int predecessor;
    Map<Integer, String> dataStore;

    public Node(Map<Integer, Node> networks, int nodeID, int fingerTableSize) {
        this.networks = networks;
        this.ID = nodeID;
        this.fingerTable = new int[fingerTableSize];
        this.dataStore = new HashMap<Integer, String>();

        //Attach to the ring.
        if (networks.size() == 0) {
            this.create();
            this.networks.put(ID, this);//Attach to the networks.
        } else {
            Object[] nodes = networks.keySet().toArray();
            Integer firstID = (Integer) nodes[0];
            int otherNodeID = firstID.intValue();//Find a node at the ring.
            this.join(otherNodeID);
        }
    }

    private int successor() {
        return fingerTable[0];
    }

    //Case 1: new node join the ring.
    //Assume this node is not at the ring currently.
    //Invoked when this node is the only node in the network.
    private void create() {
        predecessor = ID;
        for (int i = 0; i < fingerTable.length; i++) {
            fingerTable[i] = ID;
        }
    }

    //Case 1: new node join the ring.
    //Assume this node is not at the ring currently.
    //otherNode is an arbitrary node in the network.
    private void join(int otherNodeID) {
        initFingerTable(otherNodeID);
        this.networks.put(ID, this);
        updateOthers();

        //Distribute the data stored on successor node.
        Node successorNode = networks.get(this.successor());
        for (String str : successorNode.dataStore.values()) {
            this.insert(str);
        }
    }

    //Case 1: new node join the ring.
    //Assume this node is not at the ring currently.
    private void initFingerTable(int otherNodeID) {
       Node otherNode = this.networks.get(otherNodeID);
       int start = (ID + (int)Math.pow(2, 0)) % ChordSimulation.networkSize;
       this.fingerTable[0] = otherNode.findSuccessor(start);
       Node successorNode = networks.get(this.successor());
       this.predecessor = successorNode.predecessor;
       successorNode.predecessor = this.ID;
       for (int i = 1; i < fingerTable.length; i++) {
           start = (ID + (int)Math.pow(2, i)) % ChordSimulation.networkSize;
           if (isInRangeLeft(ID, fingerTable[i-1], start)) {
               fingerTable[i] = fingerTable[i-1];
           } else {
               fingerTable[i] = otherNode.findSuccessor(start);
           }
       }
    }

    //Case 1: new node join the ring.
    //Update all nodes whose finger table should refer to n.
    private void updateOthers() {
        for (int i = 0; i < fingerTable.length; i++) {
            int next = ID - (int)Math.pow(2, i);
            if(next < 0)
                next = next + ChordSimulation.networkSize;
            int successorID = this.findSuccessor(next);
            int predecessorID;
            if (successorID == next)
                predecessorID = successorID;
            else
                predecessorID = networks.get(successorID).findPredecessor(next);
            Node predecessorNode = networks.get(predecessorID);
            predecessorNode.updateFingerTable(ID, i);
        }
    }

    //Case 1: new node join the ring.
    //If s is i-th finger of n, update n's finger table with s
    private void updateFingerTable(int s, int i) {
        //if (s != ID && isInRangeLeft(ID, fingerTable[i], s)) {
        if (isInRange(ID, fingerTable[i], s)) {
            fingerTable[i] = s;
            Node p = networks.get(this.predecessor);
            p.updateFingerTable(s, i);
        }
    }

    //Case 2: some node will be down.
    //Assume this node is at the ring.
    //We will consider the data stored in this node.
    //Remove this node from the networks.
    public void remove() {
        if (networks.size() == 1) {//It is the only node in the ring.
            networks.remove(ID);
        } else {
            int successorID = this.successor();
            Node successorNode = networks.get(successorID);
            successorNode.predecessor = this.predecessor;
            this.updateOthersForDelete(ID, successorID);
            networks.remove(ID);
            //All the data stored in this node has to be distributed to other nodes.
            for (String str : dataStore.values()) {
                successorNode.insert(str);
            }
        }
    }

    //Case 2: some node will be down.
    //Update the finger table when some node is gone.
    private void updateOthersForDelete(int oldID, int newID) {
        for (int i = 0; i < fingerTable.length; i++) {
            int next = oldID - (int)Math.pow(2, i);
            if(next < 0)
                next = next + ChordSimulation.networkSize;
            int successorID = this.findSuccessor(next);
            int predecessorID;
            if (successorID == next)
                predecessorID = successorID;
            else
                predecessorID = networks.get(successorID).findPredecessor(next);
            Node predecessorNode = networks.get(predecessorID);
            predecessorNode.updateFingerTableForDelete(oldID, newID, i);
        }
    }

    //Case 2: some node will be down.
    //If o is i-th finger of n, update the finger table with n
    private void updateFingerTableForDelete(int o, int n, int i) {
        if (fingerTable[i] == o) {
            fingerTable[i] = n;
            Node p = networks.get(this.predecessor);
            p.updateFingerTableForDelete(o, n, i);
        }
    }

    //Case 3: insert value in the ring.
    public void insert(String value) {
        int key = ChordSimulation.hash(value);
        System.out.println("Inserting value: " + value + " key: " + key);
        //int successorNodeID = this.findSuccessor(key);
        int successorNodeID = this.find(key);
        Node successorNode = networks.get(successorNodeID);
        successorNode.dataStore.put(key, value);
        System.out.println("Insert value at: " + successorNodeID + "\n");
    }

    //Case 4: output the content via given key.
    public void print(int key) {
        int successorID = this.findSuccessor(key);
        Node successorNode = networks.get(successorID);
        if (successorID == key) {
            System.out.println("AT INDEX NODE");
            System.out.println(successorNode);
        } else if (successorNode.dataStore.containsKey(key)) {
            System.out.println(successorNode);
            System.out.println(successorNode.dataStore.get(key));
        } else {
            System.out.println("peer <" + key + ">: not found. \n");
        }
    }

    //Case 5: find a key
    public int find(int key) {
        int predecessor = predecessor(key);
        Node predecessorNode = networks.get(predecessor);
        int successor = predecessorNode.successor();
        System.out.println("Node " + successor + " has been visited.");
        return successor;
    }

    //Case 5: find a key
    public int predecessor(int key) {
        int predecessorID = ID;
        System.out.println("Node " + predecessorID + " has been visited.");
        while(!isInRangeRight(predecessorID, networks.get(predecessorID).successor(), key)) {
            predecessorID = networks.get(predecessorID).closestPrecedingNode(key);
            System.out.println("Node " + predecessorID + " has been visited.");
        }
        return predecessorID;
    }

    //Very important helper function. 
    //Assume this node is at the ring.
    //Depend on the findPredecessor(int) function.
    private int findSuccessor(int nodeID) {
        int predecessorID = findPredecessor(nodeID);
        Node predecessorNode = networks.get(predecessorID);
        return predecessorNode.successor();
    }

    //Very important helper function.
    //Assume this node is at the ring.
    //Depend on the closestPrecedingNode(int) function.
    private int findPredecessor(int nodeID) {
        int predecessorID = ID;
        while(!isInRangeRight(predecessorID, networks.get(predecessorID).successor(), nodeID)) {
            predecessorID = networks.get(predecessorID).closestPrecedingNode(nodeID);
        }
        return predecessorID;
    }

    //Very important helper function.
    //Assume this node is at the ring.
    //Return closest finger preceding id.
    private int closestPrecedingNode(int nodeID) {
        for (int i = fingerTable.length - 1; i >= 0; i--) {
            if (isInRange(ID, nodeID, fingerTable[i]))
                return fingerTable[i];
        }
        return ID;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("ID: ").append(ID).append("\n")
          .append("predecessor: ").append(predecessor).append("\n")
          .append("successor: ").append(successor()).append("\n");
        sb.append("FingerTable:").append("\n");
        for (int i = 0; i < fingerTable.length; i++) {
           sb.append(" fingerTable[" + i + "]: ").append(fingerTable[i]).append("\n");
        }
        sb.append("Values:\n");
        for (Entry<Integer, String> entry : dataStore.entrySet()) {
            sb.append(" Key : ").append(entry.getKey())
              .append(" Value : ").append(entry.getValue()).append("\n");
        }
        return sb.toString();
    }

    //Helper function
    //Test if nodeID is in range [left, right).
    private static boolean isInRangeLeft(int left, int right, int nodeID) {
        if (isWrapAround(left, right)) {
            if (nodeID >= left)
                return true;
            else
                return right > nodeID;
        } else {
            return nodeID >= left && right > nodeID;
        }
    }

    //Helper function
    //Test if nodeID is in range (left, right].
    private static boolean isInRangeRight(int left, int right, int nodeID) {
        if (isWrapAround(left, right)) {
            if (nodeID > left)
                return true;
            else
                return right >= nodeID;
        } else {
            return nodeID > left && right >= nodeID;
        }
    }

    //Helper function
    //Test if nodeID is in range (left, right).
    private static boolean isInRange(int left, int right, int nodeID) {
        if (isWrapAround(left, right)) {
            if (nodeID > left)
                return true;
            else
                return right > nodeID;
        } else {
            return nodeID > left && right > nodeID;
        }
    }

    //Helper function
    private static boolean isWrapAround(int left, int right) {
        if (left >= right)
            return true;
        return false;
    }
}
